package com.zb.orm.meta;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Lob;
import javax.persistence.Table;
import javax.persistence.Transient;

import org.apache.commons.lang.StringUtils;

import com.zb.util.ClassUtils;
import com.zb.util.CollectionUtil;

/**
 * 
 * 
 * 作者: zhoubang 日期：2015年3月26日 下午1:29:02
 * 
 * @param <T>
 */
public class EntityMetadata<T> {

    @SuppressWarnings("rawtypes")
    private static final Map<Class, EntityMetadata> pool = new ConcurrentHashMap<Class, EntityMetadata>();

    /**
     * <p>
     * newInstance.
     * </p>
     *
     * @param clazz
     *            a {@link java.lang.Class} object.
     * @param <E>
     *            a E object.
     */
    @SuppressWarnings("unchecked")
    public static final <E> EntityMetadata<E> newInstance(Class<E> clazz) {
        EntityMetadata<E> foo;
        if (!pool.containsKey(clazz)) {
            foo = new EntityMetadata<E>(clazz);
            pool.put(clazz, foo);
            return foo;
        }
        return pool.get(clazz);
    }

    private Class<T> clazz;
    private String tableSchema;
    private String tableName;
    private String tableCatalog;
    private Map<String, EntityColumnMetadata> fieldMap = new LinkedHashMap<String, EntityColumnMetadata>();
    private Map<String, EntityColumnMetadata> columnMap = new LinkedHashMap<String, EntityColumnMetadata>();
    private Set<String> fields;
    private Set<String> columns;
    private Set<String> columnsWithQuote;
    private Set<EntityColumnMetadata> entityColumnMetadatas;
    private String primaryKey;
    private String qualifiedTableName;

    /**
     * <p>
     * Constructor for EntityMetadata.
     * </p>
     *
     * @param clazz
     *            a {@link java.lang.Class} object.
     */
    public EntityMetadata(Class<T> clazz) {
        if (clazz == null) {
            throw new RuntimeException("create entity metadata error: class cannot be null.");
        }
        this.clazz = clazz;
        this.init();
    }

    /**
     * <p>
     * Getter for the field <code>clazz</code>.
     * </p>
     *
     * @return the clazz
     */
    public Class<T> getClazz() {
        return clazz;
    }

    /**
     * <p>
     * getColumnByField.
     * </p>
     *
     * @param fieldName
     *            a {@link java.lang.String} object.
     */
    public EntityColumnMetadata getColumnByField(String fieldName) {
        return this.fieldMap.get(fieldName);
    }

    /**
     * <p>
     * getColumnByName.
     * </p>
     *
     * @param columnName
     *            a {@link java.lang.String} object.
     */
    public EntityColumnMetadata getColumnByName(String columnName) {
        return this.columnMap.get(columnName);
    }

    /**
     * <p>
     * getAllColumnMetadatas.
     * </p>
     *
     * @return a {@link java.util.Set} object.
     */
    public Set<EntityColumnMetadata> getAllColumnMetadatas() {
        return this.entityColumnMetadatas;
    }

    /**
     * <p>
     * getAllColumnMetadatasNoPK.
     * </p>
     *
     * @return a {@link java.util.Set} object.
     */
    public Set<EntityColumnMetadata> getAllColumnMetadatasNoPK() {
        Set<EntityColumnMetadata> foo = new LinkedHashSet<EntityColumnMetadata>(this.getAllColumnMetadatas());
        foo.remove(this.getPrimaryKey());
        return foo;
    }

    /**
     * <p>
     * getAllFields.
     * </p>
     *
     * @return a {@link java.util.Set} object.
     */
    public Set<String> getAllFields() {
        return this.fields;
    }

    /**
     * <p>
     * getAllColumnNames.
     * </p>
     *
     * @return a {@link java.util.Set} object.
     */
    public Set<String> getAllColumnNames() {
        return this.columns;
    }

    /**
     * <p>
     * getAllColumnNamesWithQuote.
     * </p>
     *
     * @return a {@link java.util.Set} object.
     */
    public Set<String> getAllColumnNamesWithQuote() {
        return this.columnsWithQuote;
    }

    /**
     * <p>
     * getFieldsNoPrimaryKey.
     * </p>
     *
     * @return a {@link java.util.Set} object.
     */
    public Set<String> getFieldsNoPrimaryKey() {
        Set<String> foo = new LinkedHashSet<String>(this.getAllFields());
        foo.remove(this.getPrimaryKey().getField());
        return foo;
    }

    /**
     * <p>
     * getColumnNamesNoPrimaryKey.
     * </p>
     *
     * @return a {@link java.util.Set} object.
     */
    public Set<String> getColumnNamesNoPrimaryKey() {
        Set<String> foo = new LinkedHashSet<String>(this.getAllColumnNames());
        foo.remove(this.getPrimaryKey().getName());
        return foo;
    }

    /**
     * <p>
     * getColumnNamesWithQuoteNoPrimaryKey.
     * </p>
     *
     * @return a {@link java.util.Set} object.
     */
    public Set<String> getColumnNamesWithQuoteNoPrimaryKey() {
        Set<String> foo = new LinkedHashSet<String>(this.getAllColumnNamesWithQuote());
        foo.remove(this.getPrimaryKey().getNameWithQuote());
        return foo;
    }

    /**
     * <p>
     * Getter for the field <code>tableCatalog</code>.
     * </p>
     *
     * @return the tableCatalog
     */
    public String getTableCatalog() {
        return tableCatalog;
    }

    /**
     * <p>
     * Getter for the field <code>tableName</code>.
     * </p>
     *
     * @return the tableName
     */
    public String getTableName() {
        return tableName;
    }

    /**
     * <p>
     * Getter for the field <code>tableSchema</code>.
     * </p>
     *
     * @return the tableSchema
     */
    public String getTableSchema() {
        return tableSchema;
    }

    private void init() {
        if (!ClassUtils.hasClassAnnotation(clazz, Entity.class)) {
            throw new RuntimeException("class[" + clazz.getCanonicalName() + "] hasn't " + Entity.class.getCanonicalName() + " annotation.");
        }
        Table annotationOfTable = ClassUtils.getClassAnnotation(this.clazz, Table.class);
        this.tableName = annotationOfTable.name();
        this.tableSchema = annotationOfTable.schema();
        this.tableCatalog = annotationOfTable.catalog();

        Field[] declaredFields = this.clazz.getDeclaredFields();

        EntityColumnMetadata tempColumn = null;
        for (Field declaredField : declaredFields) {
            tempColumn = this.parseColumnMetadata(declaredField);
            if (tempColumn == null) {
                continue;
            } else if (tempColumn.isPrimaryKey()) {
                this.primaryKey = tempColumn.getField();
            }
            this.columnMap.put(tempColumn.getName(), tempColumn);
            this.fieldMap.put(tempColumn.getField(), tempColumn);
        }
        for (Method foo : this.clazz.getMethods()) {
            if (foo.getName().startsWith("get") && foo.getParameterTypes().length == 0 && foo.getReturnType() != Void.TYPE) {
                tempColumn = this.parseColumnMetadata(foo);
                if (tempColumn == null) {
                    continue;
                } else if (tempColumn.isPrimaryKey()) {
                    this.primaryKey = tempColumn.getField();
                }
                this.columnMap.put(tempColumn.getName(), tempColumn);
                this.fieldMap.put(tempColumn.getField(), tempColumn);
            }
        }

        this.fields = this.fieldMap.keySet();
        this.columns = this.columnMap.keySet();
        this.columnsWithQuote = new LinkedHashSet<String>();
        for (String foo : columns) {
            this.columnsWithQuote.add(foo);
        }
        this.entityColumnMetadatas = new HashSet<EntityColumnMetadata>(this.fieldMap.values());
        this.qualifiedTableName = this.createQualifiedTableName();
    }

    private String createQualifiedTableName() {
        final List<String> li = new ArrayList<String>();
        if (StringUtils.isNotBlank(this.tableSchema)) {
            li.add("`" + this.tableSchema + "`");
        }
        if (StringUtils.isNotBlank(this.tableCatalog)) {
            li.add("`" + this.tableCatalog + "`");
        }
        li.add("`" + this.tableName + "`");
        return CollectionUtil.join(li, ".");
    }

    /**
     * <p>
     * Getter for the field <code>qualifiedTableName</code>.
     * </p>
     *
     * @return a {@link java.lang.String} object.
     */
    public String getQualifiedTableName() {
        return this.qualifiedTableName;
    }

    /**
     * <p>
     * Getter for the field <code>primaryKey</code>.
     * </p>
     *
     */
    public EntityColumnMetadata getPrimaryKey() {
        return this.getColumnByField(this.primaryKey);
    }

    private EntityColumnMetadata parseColumnMetadata(Method f) {
        if (f == null || f.getAnnotation(Transient.class) != null) {
            return null;
        }

        Id annoId = f.getAnnotation(Id.class);
        Column annoColumn = f.getAnnotation(Column.class);
        Lob annoLob = f.getAnnotation(Lob.class);

        if (annoColumn == null) {
            return null;
        }

        final EntityColumnMetadata result = new EntityColumnMetadata();
        result.setPrimaryKey(annoId != null);

        result.setField(StringUtils.uncapitalize(f.getName().substring(3)));
        result.setFieldType(f.getReturnType());

        result.setName(annoColumn.name());
        result.setLength(annoColumn.length());
        result.setScale(annoColumn.scale());
        result.setRequire(!annoColumn.nullable());
        result.setUnique(annoColumn.unique());
        result.setLob(annoLob != null);

        if (result.isPrimaryKey()) {
            GeneratedValue annoGen = f.getAnnotation(GeneratedValue.class);
            if (annoGen != null) {
                GenerationType strategy = annoGen.strategy();
                if (strategy == GenerationType.AUTO) {
                    result.setAuto(true);
                }
            }
        }

        return result;
    }

    private EntityColumnMetadata parseColumnMetadata(Field f) {
        if (f == null || f.getAnnotation(Transient.class) != null) {
            return null;
        }

        Id annoId = f.getAnnotation(Id.class);
        Column annoColumn = f.getAnnotation(Column.class);
        Lob annoLob = f.getAnnotation(Lob.class);

        if (annoColumn == null) {
            return null;
        }

        final EntityColumnMetadata result = new EntityColumnMetadata();
        result.setPrimaryKey(annoId != null);

        result.setField(f.getName());
        result.setFieldType(f.getType());

        result.setName(annoColumn.name());
        result.setLength(annoColumn.length());
        result.setScale(annoColumn.scale());
        result.setRequire(!annoColumn.nullable());
        result.setUnique(annoColumn.unique());
        result.setLob(annoLob != null);

        if (result.isPrimaryKey()) {
            GeneratedValue annoGen = f.getAnnotation(GeneratedValue.class);
            if (annoGen != null) {
                GenerationType strategy = annoGen.strategy();
                if (strategy == GenerationType.AUTO) {
                    result.setAuto(true);
                }
            }
        }

        return result;
    }

}
